package io.noties.markwon.base.image.load

import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.os.Looper
import android.os.SystemClock
import android.text.Spanned
import android.view.View
import android.widget.TextView
import io.noties.markwon.base.image.AsyncRecycleDrawable
import io.noties.markwon.base.image.span.AsyncDrawableSpan
import io.noties.markwon.base.image.span.AsyncRecycleDrawableSpan
import io.noties.markwon.base.R

import timber.log.Timber

object AsyncDrawableOrderLazyScheduler {

    // we need a simple check if current text has already scheduled drawables
    // we need this in order to allow multiple calls to schedule (different plugins
    // might use AsyncDrawable), but we do not want to repeat the task
    //
    // hm... we need the same thing for unschedule then... we can check if last hash is !null,
    // if it's not -> unschedule, else ignore

    // @since 4.0.0
    @JvmStatic
   fun schedule(textView: TextView) {
        Timber.d("AsyncDrawableOrderLazyScheduler schedule text is span:${textView.text is Spanned}")

        val lastTextHashCode = textView.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode) as Int?

        val textHashCode = textView.text.hashCode()

        if (lastTextHashCode != null && lastTextHashCode == textHashCode) {
            return
        }
        textView.setTag(R.id.markwon_drawables_scheduler_last_text_hashcode, textHashCode)

        extractRecycleSpans(textView)?.forEach {
            it.drawable.preLoad()
        }

        val invalidator: DrawableCallbackImpl.Invalidator = TextViewInvalidator(textView)

        extractSpans(textView)?.let { spans ->
            if (textView.getTag(R.id.markwon_drawables_scheduler) == null) {
                val listener: View.OnAttachStateChangeListener =
                    object : View.OnAttachStateChangeListener {
                        override fun onViewAttachedToWindow(v: View) {}
                        override fun onViewDetachedFromWindow(v: View) {
                            unscheduled(textView)
                            v.removeOnAttachStateChangeListener(this)
                            v.setTag(R.id.markwon_drawables_scheduler, null)
                        }
                    }
                textView.addOnAttachStateChangeListener(listener)
                textView.setTag(R.id.markwon_drawables_scheduler, listener)
            }
            spans.forEach {
                it.drawable.setCallback2(
                    DrawableCallbackImpl(
                        textView,
                        invalidator,
                        it.drawable.bounds
                    )
                )
            }
        }

    }

    // must be called when text manually changed in TextView
    @JvmStatic
     fun unscheduled(view: TextView) {
        Timber.d("AsyncDrawableOrderLazyScheduler unscheduled")
        // @since 4.0.0
        view.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode) ?: return

        view.setTag(R.id.markwon_drawables_scheduler_last_text_hashcode, null)

        extractSpans(view)?.forEach{
            it.drawable.setCallback2(null)
        }

    }

    // we also could've tried the `nextSpanTransition`, but strangely it leads to worse performance
    // than direct getSpans
    private fun extractSpans(textView: TextView): List<AsyncDrawableSpan>? {
        val cs = textView.text?:return null
        val length = cs.length
        
        if(length == 0 || cs !is Spanned)
            return null

        return  cs.getSpans(0, length, AsyncDrawableSpan::class.java).asList().sortedBy { cs.getSpanStart(it) }

    }

    // we also could've tried the `nextSpanTransition`, but strangely it leads to worse performance
    // than direct getSpans
    private fun extractRecycleSpans(textView: TextView): List<AsyncRecycleDrawableSpan>? {
        val cs = textView.text?:return null
        val length = cs.length

        if(length == 0 || cs !is Spanned)
            return null

        return cs.getSpans(0, length, AsyncRecycleDrawableSpan::class.java).asList()
            .sortedBy { cs.getSpanStart(it) }
    }


    private class DrawableCallbackImpl constructor(
        private val view: TextView,
        // @since 4.1.0
        private val invalidator: Invalidator,
        initialBounds: Rect?
    ) : Drawable.Callback {
        // @since 4.1.0
        // interface to be used when bounds change and view must be invalidated
         interface Invalidator {
            fun invalidate()
        }

        private var previousBounds: Rect
        override fun invalidateDrawable(who: Drawable) {
            Timber.d("DrawableCallbackImpl invalidateDrawable:${who}")
            if (Looper.myLooper() != Looper.getMainLooper()) {
                view.post { invalidateDrawable(who) }
                return
            }
            val rect = who.bounds

            // okay... the thing is IF we do not change bounds size, normal invalidate would do
            // but if the size has changed, then we need to update the whole layout...
            if (previousBounds != rect) {
                // @since 4.1.0
                // invalidation moved to upper level (so invalidation can be deferred,
                // and multiple calls combined)
                invalidator.invalidate()
                previousBounds = Rect(rect)
            } else {
                Timber.d("DrawableCallbackImpl invalidateDrawable is self")

                if(who is AsyncRecycleDrawable){
                    invalidator.invalidate()
                }else{
                    view.postInvalidate()
                }

            }
        }

        override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) {
            Timber.d("DrawableCallbackImpl scheduleDrawable:%s",who.toString())
            val delay = `when` - SystemClock.uptimeMillis()
            view.postDelayed(what, delay)
        }

        override fun unscheduleDrawable(who: Drawable, what: Runnable) {
            Timber.d("DrawableCallbackImpl unscheduleDrawable:%s",who.toString())
            view.removeCallbacks(what)
        }

        init {
            previousBounds = Rect(initialBounds)
        }
    }

    private class TextViewInvalidator  constructor(private val textView: TextView) :
        DrawableCallbackImpl.Invalidator, Runnable {
        override fun invalidate() {
            textView.removeCallbacks(this)
            textView.post(this)
        }

        override fun run() {
            val preText = textView.text
            textView.text = preText
        }
    }
}